package org.valkyrie.gwt.bean.dev.generator;

import java.beans.PropertyDescriptor;
import java.io.PrintWriter;
import java.lang.reflect.Method;

import org.apache.commons.beanutils.PropertyUtils;
import org.valkyrie.gwt.bean.client.BeanManager;
import org.valkyrie.gwt.bean.client.ReflectedBean;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

/**
 * Generator of {@link BeanManager} from {@link ReflectedBean} class<br/>
 * <i> This generator requires the Apache Common Bean Utils</i>
 * 
 * @author Pierre
 * 
 */
public class BeanManagerGenerator extends Generator {

	private static final String MANAGER_CLASS_NAME_SUFFIX = "Manager";
	private static final String MANAGER_CLASS_DEFAULT_PACKAGE = "org.valkyrie.gwt.bean.client.managers";
	private static final String MANAGER_SUPER_CLASS = BeanManager.class
			.getName();

	private TreeLogger logger;
	private GeneratorContext context;

	public String generate(TreeLogger logger, GeneratorContext context,
			String requestedBean) throws UnableToCompleteException {
		this.logger = logger;
		this.context = context;

		TypeOracle typeOracle = context.getTypeOracle();
		assert (typeOracle != null);

		JClassType requestedBeanClass = typeOracle.findType(requestedBean);
		if (requestedBeanClass == null) {
			logger.log(TreeLogger.ERROR, "Unable to find metadata for type '"
					+ requestedBean + "'", null);
			throw new UnableToCompleteException();
		}

		// Generate the name and the package of the manager class
		String beanClassFullName = requestedBeanClass.getPackage().getName()
				+ "." + requestedBeanClass.getName();
		String managerName = requestedBeanClass.getName()
				+ MANAGER_CLASS_NAME_SUFFIX;
		String managerPackage = MANAGER_CLASS_DEFAULT_PACKAGE + "."
				+ requestedBeanClass.getPackage().getName();
		String managerFullName = managerPackage + "." + managerName;

		// Initialize the source writer
		SourceWriter src = initManagerClass(requestedBeanClass, managerName,
				managerPackage, beanClassFullName);
		if (src == null)
			return managerFullName;

		// Generate the source code of the manager
		generateGettersAndSettersInvokers(requestedBeanClass, src,
				beanClassFullName);

		// Finalize the source writer
		finalizeManagerClass(src);

		// Return the manager full name
		return managerFullName;
	}

	/**
	 * Generate the source code of the manager
	 * 
	 * @param requestedBeanClass
	 * @param src
	 */
	private void generateGettersAndSettersInvokers(
			JClassType requestedBeanClass, SourceWriter src,
			String beanClassName) throws UnableToCompleteException {

		// Find the bean class
		Class<?> beanClass = null;
		String jBeanClass = requestedBeanClass.getPackage().getName() + "."
				+ requestedBeanClass.getName();
		try {
			beanClass = Class.forName(jBeanClass);
		} catch (Exception e) {
			logger.log(TreeLogger.ERROR, "Unable to find class  '" + jBeanClass
					+ "'", null);
			throw new UnableToCompleteException();
		}

		// Describe the bean properties
		PropertyDescriptor[] properties = PropertyUtils
				.getPropertyDescriptors(beanClass);

		// GETTERS
		// Generate the method 'protected Object invokeGetter(Object bean,
		// String property)'
		src.println("protected Object invokeGetter(" + beanClassName
				+ " bean, String property) throws ReflectionException {");
		for (PropertyDescriptor p : properties) {

			Method readMethod = p.getReadMethod();
			String name = p.getName();
			if (readMethod == null) // If the property cannot be read
				continue;

			// Invoke the getter on the bean
			src.println("if (property.equals(\"" + name + "\"))");
			src.println("return bean." + readMethod.getName() + "();");

		}
		src
				.println("throw new ReflectionException(\"The property cannot be get\");");
		src.println("}");

		// SETTERS
		// Generate the method 'protected void invokeSetter(B bean, String
		// property, Object value)'
		src
				.println("protected void invokeSetter("
						+ beanClassName
						+ " bean, String property, Object value) throws ReflectionException {");
		for (PropertyDescriptor p : properties) {

			Method writeMethod = p.getWriteMethod();
			String name = p.getName();
			if (writeMethod == null) // If the property cannot be write
				continue;

			// Invoke the setter on the bean
			src.println("if (property.equals(\"" + name + "\")) {");
			String propertyBasedClass = p.getWriteMethod().getDeclaringClass()
					.getName();
			src
					.println("(("
							+ propertyBasedClass
							+ ")bean)."
							+ writeMethod.getName()
							+ "(("
							+ validatePropertyTypeForCast(p.getPropertyType()
									.getName()) + ")value);");
			src.println("return;");
			src.println("}");

		}
		src
				.println("throw new ReflectionException(\"The property cannot be set\");");
		src.println("}");

	}

	/**
	 * Initialize the creation of the manager class
	 */
	private SourceWriter initManagerClass(JClassType requestedBeanClass,
			String managerName, String managerPackage, String beanClassFullName) {

		// Create the source writer and return it
		PrintWriter printWriter = context.tryCreate(logger, managerPackage,
				managerName);
		if (printWriter == null)
			return null;
		ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
				managerPackage, managerName);
		String superClassNameFull = MANAGER_SUPER_CLASS + "<"
				+ beanClassFullName + ">";
		composerFactory.setSuperclass(superClassNameFull);
		composerFactory
				.addImport("org.valkyrie.gwt.bean.client.ReflectionException");
		return composerFactory.createSourceWriter(context, printWriter);
	}

	/**
	 * Finalize the creation of the manager class
	 * 
	 * @param sourceWriter
	 */
	private void finalizeManagerClass(SourceWriter sourceWriter) {
		sourceWriter.commit(logger);
	}

	private String validatePropertyTypeForCast(String value) {
		if (value.equals("boolean")) {
			return "Boolean";
		}
		if (value.equals("int")) {
			return "Integer";
		}
		if (value.equals("double")) {
			return "Double";
		}
		if (value.equals("float")) {
			return "float";
		}
		if (value.equals("char")) {
			return "Character";
		}
		if (value.equals("byte")) {
			return "Byte";
		}
		if (value.equals("short")) {
			return "Short";
		}
		if (value.equals("long")) {
			return "Long";
		}
		return value;
	}
}
